﻿using System;
using System.Text;
using System.IO;

namespace Xenta.Utils
{
    /// <summary>
    /// Represents the Base85 form of binary-to-text encoding.
    /// </summary>
    /// <remarks>
    /// By using five ASCII characters to represent four bytes of binary data.
    /// Making the encoded size 25% larger than the original, assuming 8 bits 
    /// per ASCII character.
    /// </remarks>
    public static class Base85
    {
        #region Constants

        /// <summary>
        /// Prefix mark that identifies an encoded Base85 string.
        /// </summary>
        public const string PREFIX_MARK = "<~";

        /// <summary>
        /// Suffix mark that identifies an encoded Base85 string.
        /// </summary>
        public const string SUFFIX_MARK = "~>";

        private const int ENC_BLOCK_LEN = 5;
        private const int DEC_BLOCK_LEN = 4;
        private const int ASCII_OFFSET = 33;

        private static readonly uint[] POW85 =
        {
            85 * 85 * 85 * 85, 
            85 * 85 * 85, 
            85 * 85, 
            85, 
            1
        };

        #endregion

        #region Methods

        /// <summary>
        /// Decodes an Base85 encoded string into the original binary data.
        /// </summary>
        /// <param name="s">Base85 encoded string</param>
        /// <param name="enableMarks">Indicates when the prefix/suffix marks is presented for encoding.</param>
        /// <returns>The byte array of decoded binary data.</returns>
        public static byte[] Decode(string s, bool enableMarks = true)
        {
            if(enableMarks && (!s.StartsWith(PREFIX_MARK) || !s.EndsWith(SUFFIX_MARK)))
                throw new ArgumentException("Base85: Encoded data does not contain prefix/suffix.");

            if(s.StartsWith(PREFIX_MARK))
                s = s.Substring(PREFIX_MARK.Length);
            if(s.EndsWith(SUFFIX_MARK))
                s = s.Substring(0, s.Length - SUFFIX_MARK.Length);

            using(var ms = new MemoryStream())
            {
                int count = 0;
                uint tuple = 0;
                var block = new byte[DEC_BLOCK_LEN];

                foreach(char c in s)
                {
                    bool processChar = false;
                    switch(c)
                    {
                        case 'z':
                            if(count != 0)
                                throw new Exception("Base85: The character 'z' is invalid inside an ASCII85 block.");
                            block[0] = 0;
                            block[1] = 0;
                            block[2] = 0;
                            block[3] = 0;
                            ms.Write(block, 0, block.Length);
                            processChar = false;
                            break;
                        case '\n':
                        case '\r':
                        case '\t':
                        case '\0':
                        case '\f':
                        case '\b':
                            processChar = false;
                            break;
                        default:
                            if(c < '!' || c > 'u')
                                throw new Exception("Base85: Bad character found.");
                            processChar = true;
                            break;
                    }
                    if(!processChar)
                        continue;

                    tuple += ((uint)(c - ASCII_OFFSET) * POW85[count]);
                    count++;
                    if(count != ENC_BLOCK_LEN)
                        continue;

                    DecodeBlock(block, block.Length, tuple);
                    ms.Write(block, 0, block.Length);

                    tuple = 0;
                    count = 0;
                }

                if(count != 0)
                {
                    if(count == 1)
                        throw new Exception("Base85: The last block data cannot be a single byte.");
                    count--;
                    tuple += POW85[count];
                    DecodeBlock(block, count, tuple);
                    ms.Write(block, 0, count);
                }

                return ms.ToArray();
            }
        }

        /// <summary>
        /// Encodes binary data into a plaintext Base85 format string.
        /// </summary>
        /// <param name="data">The binary data to encode.</param>
        /// <param name="enableMarks">Indicates when the prefix/suffix marks is presented for encoding.</param>
        /// <returns>The Base85 encoded string.</returns>
        public static string Encode(byte[] data, bool enableMarks = true)
        {
            var sb = new StringBuilder(data.Length * (ENC_BLOCK_LEN / DEC_BLOCK_LEN));
            if(enableMarks)
                sb.Append(PREFIX_MARK);

            int count = 0;
            uint tuple = 0;
            var block = new byte[ENC_BLOCK_LEN];

            foreach(byte b in data)
            {
                if(count >= DEC_BLOCK_LEN - 1)
                {
                    tuple |= b;
                    if(tuple == 0)
                        sb.Append('z');
                    else
                        EncodeBlock(sb, block, block.Length, tuple);
                    tuple = 0;
                    count = 0;
                }
                else
                {
                    tuple |= (uint)(b << (24 - (count * 8)));
                    count++;
                }
            }
            if(count > 0)
                EncodeBlock(sb, block, count + 1, tuple);
            if(enableMarks)
                sb.Append(SUFFIX_MARK);
            return sb.ToString();
        }

        #endregion

        #region Utils

        private static void EncodeBlock(StringBuilder sb, byte[] block, int num, uint tuple)
        {
            for(int i = block.Length - 1; i >= 0; i--)
            {
                block[i] = (byte)((tuple % 85) + ASCII_OFFSET);
                tuple /= 85;
            }
            for(int i = 0; i < num; i++)
            {
                char c = (char)block[i];
                sb.Append(c);
            }
        }

        private static void DecodeBlock(byte[] block, int num, uint tuple)
        {
            for(int i = 0; i < num; i++)
                block[i] = (byte)(tuple >> 24 - (i * 8));
        }

        #endregion
    }
}